<?php
/**
 * Created by PhpStorm.
 * User: inhere
 * Date: 2016/9/27
 * Time: 14:17
 */

namespace app\components;

use App;

/**
 * simple file logger handler
 * Class ImageSerial
 * @package app\components
 */
class ImageSerial
{
    /**
     * \PDO instance
     * @var \PDO
     */
    private $db;

    const SERIAL_LENGTH = 65;

    const TABLE_USER = 'tUserInfo';
    const TABLE_PDT = 'tProduct';
    const TABLE_NAME = 'tImgSerialNumber';

    const IMG_SERIAL_STR = '1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65';

    protected $cachePath = '@temp/caches';

    public function __construct(\PDO $pdo = null)
    {
        $this->db = $pdo;
    }

    /**
     * @param $userId
     * @param $productId
     * @return bool
     */
    public function addPackTask($userId, $productId)
    {
        // check 'userId' and 'productId'
        if ( !$this->checkUserAndProduct($userId, $productId) ) {
            App::logger()->error("User or product do not exists! userId: $userId, productId: $productId" );

            return App::output()->formatJson(80, 'User or product do not exists!');
        }

        $binds = [$userId, $productId];
        $table = ImageSerial::TABLE_NAME;
        $sn = ImageSerial::genSerialNumber($userId, $productId);
        $isn = $this->getISN($sn, true,',');

        $binds[] = $sn;
        $binds[] = $isn;
        $binds[] = time();
        $sql = "INSERT INTO {$table} (iUserId,iProductId,sSerialNumber,sISN,iCreated) VALUES (?,?,?,?,?)";

        App::logger()->debug('Run SQL: '. $sql, ['binds' => $binds]);

        $stmt = App::pdo()->prepare($sql);
        if ( !$stmt->execute($binds) ) {
            App::logger()->error("Add task failed!, userId: $userId, productId: $productId, ISN: $isn");

            return false;
        }

        App::logger()->info("Add a new task, userId: $userId, productId: $productId, ISN: $isn");

        return true;
    }

    /**
     * get image serial number
     * 通过生成的序列号得到图片序列
     *     序列号是65位的16进制字符串
     *     把上面的初始图片序列看作第一行，将第一个数移到最后生成第二行，以此类推 ... 生成16行(对应16进制)的表格(16x65)
     *
     *     序列号的每一位数:
     *         所在位置对应上面表格的所在列
     *         数值对应所在行
     *     来找到对应的图片序号，组合成一个有65个元素的列表
     * @param  string $sn 序列号
     * @param bool $toString
     * @param string $sp
     * @return array|string
     */
    public function getISN($sn, $toString = false, $sp = ' ')
    {
        if (!$sn || self::SERIAL_LENGTH !== strlen($sn)) {
            return App::output()->formatJson(134,'Missing parameter!');
        }

        $list = [];
        $table = $this->genImageNumberTable();

        foreach (str_split($sn) as $index => $char) {
            $num = base_convert($char, 16, 10);
            $list[] = $table[$num][$index];
        }

        return $toString ? implode($sp, $list) : $list;
    }

    /**
     * generate a serial number
     * @description
     *     - (可能会重复)将 userId 和 productId 通过MD5生成唯一字符串(16进制，32位长度)，再转成2进制，从第一位开始截取65位。
     *     - (更好)将 userId 和 productId 通过SHA256生成唯一字符串(16进制，64位长度)，再添加一个随机数补到65位。
     * @param int $userId
     * @param int $productId
     * @return string
     */
    public static function genSN($userId, $productId)
    {
        return self::genSerialNumber($userId, $productId);
    }
    public static function genSerialNumber($userId, $productId)
    {
        // generate new serial number use md5
        // $key = md5($userId . $productId);
        // $binStr = base_convert(substr($key,0,16), 16, 2) . base_convert(substr($key,16), 16, 2);
        // $serialNum = substr($binStr, 0, 65);

        // $last = rand(0,15);// 同样的参数最后一个字符会出现不同 base_convert($last, 10, 16)
        $last = substr(hash('md4', $userId, false),0,1); // 保证最后一个字符相同

        // generate new serial number use sha256
        return hash('sha256', $userId . $productId, false) . $last;
    }

    public function saveToDb($sn)
    {
        try {
            $table = self::TABLE_NAME;
            $binds[] = $sn;
            $binds[] = time();
            $sql = "INSERT INTO {$table} (iUserId,iProductId,sSerialNumber,iCreated) VALUES (?,?,?,?)";

            App::logger()->debug('Run SQL: '. $sql, ['binds' => $binds]);

            $stmt = $this->db->prepare($sql);
            $stmt->execute($binds);
            $ret = $stmt->rowCount();
        } catch(\Exception $e) {
            throw $e;
        }
    }

    public function setDb(\PDO $pdo)
    {
        $this->db = $pdo;

        return $this;
    }

    /**
     * generate image number table
     * @return array
     */
    public function genImageNumberTable()
    {
        // todo check image serial table cache.
        $cachePath = App::alias($this->cachePath);
        $fileName = 'img-num-table.json';
        $cacheFile = $cachePath . '/' . $fileName;

        // cache exist
        if (is_file($cacheFile) && ($json = file_get_contents($cacheFile))) {
            return json_decode($json, true);
        }

        $firstRow = explode(',', self::IMG_SERIAL_STR);
        $table = [$firstRow];
        $nextRow = $firstRow;

        for ($i=0; $i < 15; $i++) {
            $firstCol = array_shift($nextRow);
            $nextRow[] = $firstCol;
            $table[] = $nextRow;
        }

        // set cache
        if (!is_dir($cachePath)) {
            @mkdir($cachePath,0775,true);
        }
        $json = json_encode($table);

        if ( !file_put_contents($cacheFile, $json) ) {
            throw new \RuntimeException("Write cache data is failure. File: $cacheFile");
        }

        return $table;
    }

    protected function checkUserAndProduct($userId, $productId)
    {
        // in debugging
        if (APP_DEBUG) {
            return true;
        }

        $table = self::TABLE_USER;
        $sql = "SELECT iUserId FROM {$table} WHERE iUserId=? LIMIT 1";
        $binds = [$userId];

        App::logger()->debug('Run SQL: '. $sql, ['binds' => $binds]);

        $stmt = App::pdo()->prepare($sql);
        $stmt->execute($binds);
        if( !$stmt->fetch(\PDO::FETCH_ASSOC) ) {
            return false;
        }

        $table = self::TABLE_PDT;
        $sql = "SELECT iProductId FROM {$table} WHERE iProductId=? LIMIT 1";
        $binds = [$productId];

        App::logger()->debug('Run SQL: '. $sql, ['binds' => $binds]);

        $stmt = App::pdo()->prepare($sql);
        $stmt->execute($binds);
        if( !$stmt->fetch(\PDO::FETCH_ASSOC) ) {
            return false;
        }

        return true;
    }
}
